Drzewa decyzyjne są graficzną metodą wspomagania procesu decyzyjnego. Jest to jedna z najczęściej wykorzystywanych technik analizy danych. Drzewa składają się z korzenia oraz gałęzi prowadzących z korzenia do kolejnych wierzchołków. Wierzchołki, z których wychodzi co najmniej jedna krawędź, są nazywane węzłami, a pozostałe wierzchołki – liśćmi. W każdym węźle sprawdzany jest pewien warunek dotyczący danej obserwacji, i na jego podstawie wybierana jest jedną z gałęzi prowadząca do kolejnego wierzchołka. Klasyfikacja danej obserwacji polega na przejściu od korzenia do liścia i przypisaniu do tej obserwacji klasy zapisanej w danym liściu.
Drzewa decyzyjne znajdują praktyczne zastosowanie w różnego rodzaju problemach decyzyjnych, szczególnie takich gdzie występuje dużo rozgałęziających się wariantów, a także w warunkach ryzyka. Wiele algorytmów uczenia się wykorzystuje drzewa decyzyjne do reprezentacji hipotez. Zgodnie z ogólnym celem uczenia się indukcyjnego, dążą one do uzyskania drzewa decyzyjnego klasyfikującego przykłady trenujące z niewielkim błędem próbki i o możliwie niewielkim rozmiarze, w nadziei, że takie drzewo będzie miało również niewielki błąd rzeczywisty. Drzewa decyzyjne znajdują szerokie zastosowanie w problemach związanych z klasyfikacją i predykcją pojęć typu:
Chcemy zautomatyzować proces przyjmowania kandydatów na praktyki w dużej firmie. Posiadamy setki przykładów z przeszłości, chcemy wydobyć z nich reguły decyzyjne.
Atrybuty:
są kodowane skalą od 1 do 5.
Na podstawie tabeli decyzyjnej tworzymy drzewo, którego węzłami są poszczególne atrybuty, gałęziami - wartości odpowiadające tym atrybutom, a liście tworzą poszczególne decyzje. Na podstawie przykładowych danych wygenerowano następujące drzewo:
Drzewo w takiej postaci odzwierciedla, w jaki sposób na podstawie atrybutów były podejmowane decyzje klasyfikujące.
Algorytm działa rekurencyjnie dla każdego węzła drzewa. Musimy podjąć decyzję, czy węzeł będzie:
Zastosowanie uczenia maszynowego do generowania drzew decyzyjnych to automatyczne wykrywanie wzorców w danych i tworzenie na ich podstawie schematów podejmowania decyzji.
Algorytm CART (ang. Classification and Regression Trees) bazuje na wykorzystaniu struktury drzewa binarnego do predykcji – zarówno klasyfikacji, jak i regresji.
### Kryteria oceny trafności przewidywania: Największą trafność ma model o najmniejszym koszcie klasyfikacji (w większości przypadków przyjmuje się za koszt stosunek przypadków błędnie sklasyfikowanych do wszystkich przypadków).
### Wybór podziału:
W każdym węźle drzewa wyszukiwany jest podział dający najlepszą trafność predykcji. Najczęściej w metodzie CART stosowane są następujące reguły podziałów: indeks Giniego, miara entropii i reguła podziału na dwie części.
Rozszczepianie można przeprowadzać tak długo, aż uzyska się klasyfikację doskonałą. Jednak nie w tym celu stosuje się drzewa decyzyjne. Dlatego też w analizie określa się warunek zatrzymania procesu rozszczepiania. Dla kontroli tego procesu ustala się m.in. minimalną liczność węzłów. Wtedy proces podziału będzie tak długo przeprowadzany aż węzły końcowe będą jednorodne, bądź zawierać będą co najwyżej ustaloną liczbę przypadków.
Dobór właściwego rozmiaru drzewa stanowi istotny problem metody CART. Z jednej strony drzewo powinno być jak najprostsze, ale z drugiej strony powinno też prezentować złożoność danych.
Nadmiernie dopasowane drzewo charakteryzuje się małym błędem dla danych treningowych, jednak dla danych testowych błąd jest wysoki. Zabieg przycinania ma wyeliminować potencjalny overfitting.
Posiadając dane pasażerów okrętu Titanic wytrenuj drzewo decyzyjne, które jako wynik końcowy będzie klasyfikował czy dana osoba przeżyła, czy też nie.
import pandas as pd # wizualizacja i wstępna obróbka danych
import numpy as np # wizualizacja, wymiary etc
import matplotlib.pyplot as plt # wykresy, histogramy
from sklearn.tree import DecisionTreeClassifier # klasyfikator drzew decyzyjnyc
from sklearn import tree
from sklearn.model_selection import train_test_split # dzielenie zbiorów
from sklearn import metrics # acc
from sklearn.metrics import plot_confusion_matrix # macierz pomyłek
from sklearn.preprocessing import StandardScaler # obróbka danych
import seaborn as sns
import plotly.express as px
from sklearn.tree import export_graphviz
from six import StringIO
from IPython.display import Image
import pydotplus
from sklearn.model_selection import GridSearchCV
from IPython.display import clear_output
def Pozostalosci(data_train):
# tutaj mamy ładnie wypisane ile rekordów pozostało w zbiorze
# oraz ile % obserwacji posiada jakiekolwiek braki w danych
print('Pozostało ' + str(data_train.shape[0]) + ' obserwacji.')
print(str(round(data_train.isnull().any(axis=1).sum() / data_train.shape[0] * 100, 2)) + '% obserwacji zawiera braki w danych.')
def WykrKolowy(valTab, labTab, pltTitle):
fig = px.pie(values=valTab, names=labTab, color_discrete_sequence=px.colors.sequential.Viridis)
fig.update_traces(textposition='inside', textinfo='percent+label+value',marker=dict(line=dict(color='#FFFFFF', width=4)),textfont_size=15, title=pltTitle)
fig.show()
def Histogram(valTab, title):
plt.figure(figsize=(15, 9))
plt.title(title)
plt.xlabel('Value')
plt.ylabel('Density')
plt.hist(valTab, density=True)
plt.show()
def Klasyfikator(params, X_train, y_train, X_test, y_test):
resTable = [] # tablica, zawierająca dokładności klasyfikatora na zbiorze testowym
bestRes = 0.0 # zmienna, przechowująca najwyższy wynik dokładności
tree_clas = DecisionTreeClassifier(random_state=5) # klasyfikator
cvTable = [] # tablica, zawierająca wartości kross-walidacji
for i in np.arange(2,20):
clear_output(wait=True) # czyszczenie output
print(f'Postęp uczenia klasyfikatora: {(i/20)*100:.2f}%')
grid_search = GridSearchCV(estimator=tree_clas, param_grid=param_grid, cv=i, verbose=False).fit(X_train, y_train)
final_model = grid_search.best_estimator_ # najlepszy model dla zadanego CV
resTable.append(final_model.score(X_test,y_test)) # dołączenie wyniku dokładności do tablicy
cvTable.append(i) # dołączenie wartości kross-walidacji
if final_model.score(X_test,y_test) > bestRes:
bestRes = final_model.score(X_test,y_test) # zmiana wartości zmiennej gdy klasyfikator uzyska lepszy wynik
final_modelX = final_model # zmienna, przechowująca model klasyfikatora najwyższego wyniku dokładności
return resTable, cvTable, bestRes, final_modelX
Za pomocą 'pandas' ustawiamy formatowanie zmiennych ciągłych z precyzją do dwóch miejsc po przecinku oraz ustawiamy wyświetlanie wszystkich kolumn i wierszy.
pd.set_option('float_format', '{:.2f}'.format)
pd.set_option("display.max_columns", 999)
pd.set_option("display.max_rows", 999)
# wczytanie zbioru treningowego oraz testowego
# UWAGA
# zbiór testowy nie zawiera kolumny 'Survived',
# więc zbiór treningowy traktujemy jako X + y
# i sam zbiór treningowy rozbijemy na X_train, X_test, y_train oraz y_test
data_train = pd.read_csv("train.csv", header=0)
data_test = pd.read_csv("test.csv", header=0)
# ilość rekordów
Pozostalosci(data_train)
# pięć pierwszych próbek
data_train.head(10)
Pozostało 891 obserwacji. 79.46% obserwacji zawiera braki w danych.
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.00 | 1 | 0 | A/5 21171 | 7.25 | NaN | S |
| 1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.00 | 1 | 0 | PC 17599 | 71.28 | C85 | C |
| 2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.00 | 0 | 0 | STON/O2. 3101282 | 7.92 | NaN | S |
| 3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.00 | 1 | 0 | 113803 | 53.10 | C123 | S |
| 4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.00 | 0 | 0 | 373450 | 8.05 | NaN | S |
| 5 | 6 | 0 | 3 | Moran, Mr. James | male | NaN | 0 | 0 | 330877 | 8.46 | NaN | Q |
| 6 | 7 | 0 | 1 | McCarthy, Mr. Timothy J | male | 54.00 | 0 | 0 | 17463 | 51.86 | E46 | S |
| 7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.00 | 3 | 1 | 349909 | 21.07 | NaN | S |
| 8 | 9 | 1 | 3 | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) | female | 27.00 | 0 | 2 | 347742 | 11.13 | NaN | S |
| 9 | 10 | 1 | 2 | Nasser, Mrs. Nicholas (Adele Achem) | female | 14.00 | 1 | 0 | 237736 | 30.07 | NaN | C |
# sprawdzamy typy danych w poszczególnych kolumnach,
# czy kolumna zawiera jakieś braki w wartościach
# oraz ilość tych braków
summary = pd.DataFrame(data_train.dtypes, columns=['Dtype'])
summary['Nulls'] = pd.DataFrame(data_train.isnull().any())
summary['Sum'] = pd.DataFrame(data_train.isnull().sum())
summary.Dtype = summary.Dtype.astype(str)
print(summary)
# jak można zauważyć są trzy kolumny, które zawierają braki
# Age
# Embarked
# Cabin
# dlatego z czystym sumieniem 'uznajemy że wartości z kolumny 'Cabin',
# zważywszy na ilość brakujących danych nie powinny brać udziału jako argument
# do klasyfikacji'
Dtype Nulls Sum PassengerId int64 False 0 Survived int64 False 0 Pclass int64 False 0 Name object False 0 Sex object False 0 Age float64 True 177 SibSp int64 False 0 Parch int64 False 0 Ticket object False 0 Fare float64 False 0 Cabin object True 687 Embarked object True 2
data_train = data_train[~data_train.Age.isnull()]
data_train = data_train[~data_train.Embarked.isnull()]
data_train = data_train.drop('Cabin', axis=1)
Pozostalosci(data_train)
Pozostało 712 obserwacji. 0.0% obserwacji zawiera braki w danych.
data_train['Survived'].value_counts()
0 424 1 288 Name: Survived, dtype: int64
WykrKolowy([424,288],['martwy/a','żywy/a'],'Wykres kołowy względem przeżycia')
data_train['Sex'].value_counts()
male 453 female 259 Name: Sex, dtype: int64
WykrKolowy([453,259],['male','female'],'Wykres kołowy względem płci pasażerów')
data_train['Pclass'].value_counts()
3 355 1 184 2 173 Name: Pclass, dtype: int64
WykrKolowy([184,173,355],['Klasa 1','Klasa 2','Klasa 3'],'Wykres kołowy względem posiadanej klasy')
data_train.Age.describe()
count 712.00 mean 29.64 std 14.49 min 0.42 25% 20.00 50% 28.00 75% 38.00 max 80.00 Name: Age, dtype: float64
Histogram(data_train['Age'], 'Histogram of Age')
adultsCount = 0
childrenCount = 0
for i in range(len(data_train.Fare)):
if np.array(data_train.Age)[i] >= 18:
adultsCount += 1
elif np.array(data_train.Age)[i] > 0 and np.array(data_train.Age)[i] < 18:
childrenCount += 1
print(f'Liczba pełnoletnich pasażerów: {adultsCount}')
print(f'Liczba niepełnoletnich pasażerów: {childrenCount}')
Liczba pełnoletnich pasażerów: 599 Liczba niepełnoletnich pasażerów: 113
data_train['Embarked'].value_counts()
S 554 C 130 Q 28 Name: Embarked, dtype: int64
WykrKolowy([554,130,28],['Southampton','Cherbourg','Queenstown'],'Wykres kołowy względem portu docelowego')
data_train['SibSp'].value_counts()
0 469 1 183 2 25 4 18 3 12 5 5 Name: SibSp, dtype: int64
WykrKolowy([469,183,25,18,12,5],['zero','jeden','dwa','trzy','cztery','pięć'],'Wykres kołowy względem liczby rodzeństwa / małżonków na pokładzie')
data_train['Parch'].value_counts()
0 519 1 110 2 68 3 5 5 5 4 4 6 1 Name: Parch, dtype: int64
WykrKolowy([519,110,68,5,5,4,1],['zero','jeden','dwa','trzy','cztery','pięć','sześć'],'Wykres kołowy względem liczby dzieci / rodziców na pokładzie')
data_train.Fare.describe()
count 712.00 mean 34.57 std 52.94 min 0.00 25% 8.05 50% 15.65 75% 33.00 max 512.33 Name: Fare, dtype: float64
Histogram(data_train['Fare'], 'Histogram of Fare')
print(f'Największe opłaty:')
for i in range(len(data_train.Fare)):
if np.array(data_train.Fare)[i] > 512:
print(f'Opłata = {np.array(data_train.Fare)[i]} {np.array(data_train.Name)[i]}')
print(f'\nNajmniejsze opłaty:')
for i in range(len(data_train.Fare)):
if np.array(data_train.Fare)[i] == 0:
print(f'Opłata = {np.array(data_train.Fare)[i]} {np.array(data_train.Name)[i]}')
Największe opłaty: Opłata = 512.3292 Ward, Miss. Anna Opłata = 512.3292 Cardeza, Mr. Thomas Drake Martinez Opłata = 512.3292 Lesurer, Mr. Gustave J Najmniejsze opłaty: Opłata = 0.0 Leonard, Mr. Lionel Opłata = 0.0 Harrison, Mr. William Opłata = 0.0 Tornquist, Mr. William Henry Opłata = 0.0 Johnson, Mr. William Cahoone Jr Opłata = 0.0 Johnson, Mr. Alfred Opłata = 0.0 Andrews, Mr. Thomas Jr Opłata = 0.0 Reuchlin, Jonkheer. John George
data_train = data_train.replace('male', 1)
data_train = data_train.replace('female', 0)
# Embarked:
# (C = Cherbourg; Q = Queenstown; S = Southampton)
# (C = 0; Q = 1; S = 2)
data_train = data_train.replace('C', 0)
data_train = data_train.replace('Q', 1)
data_train = data_train.replace('S', 2)
data_train = data_train.drop(['Name', 'Ticket', 'PassengerId'], axis=1)
X = data_train.drop(['Survived'], axis=1)
y = data_train['Survived']
colormap = plt.cm.viridis
plt.figure(figsize=(12,12))
plt.title('Macierz korelacji', y=1.05, size=15)
sns.heatmap(data_train.astype(float).corr(),linewidths=0.1,vmax=1.0, square=True, cmap=colormap, linecolor='white', annot=True)
plt.show()
sScaler = StandardScaler(with_std=True, with_mean=True)
X = sScaler.fit(X).transform(X)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
param_grid = {'max_features': ['auto', 'sqrt', 'log2'],
'ccp_alpha': np.arange(0.001,0.1,0.01),
'max_depth' : np.arange(2,20),
'criterion' :['gini', 'entropy']
}
resTable, cvTable, bestRes, final_model = Klasyfikator(param_grid, X_train, y_train, X_test, y_test)
Postęp uczenia klasyfikatora: 95.00%
final_model
DecisionTreeClassifier(ccp_alpha=0.001, criterion='entropy', max_depth=5,
max_features='auto', random_state=5)
bestRes
0.822429906542056
Histogram(resTable, 'Histogram of score')
fig, ax = plt.subplots(figsize=(6, 6))
normalize = None # możliwości: 'true', None
plot = plot_confusion_matrix(final_model, X_test, y_test,
cmap=plt.cm.Greens,
normalize='true', ax=ax, values_format='.2f')
plot.ax_.set_title('Macierz pomyłek dla zbioru testowego')
plt.tight_layout()